package org.xmlbinder;

import org.xmlpull.v1.XmlSerializer;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.logging.Level;
import java.util.*;
import java.io.IOException;

/**This class uses the metadata on the Fields to inject data into a Field
 * 1) Acts as the data structure for caching the information of a bean.
 * 2) Injects the data into a field
 * @author Noble Paul (noble.paul@gmail.com)
 *         Date: Dec 15, 2007
 *         Time: 2:55:17 PM
 */
public class FieldInjector {
    Class type;
    String name;
    boolean isElem = false, isCdata = false,isAttr = false, isEmpty = false,isArray = false,isStartTag = false,
            isEndTag = false,isList = false,isMap = false,isProp = false,isCollection = false,isSet = false;
    BeanAnnotationProcessor bean;
    Field field;
    Method setter,getter;
    BeanAnnotationProcessor parent;

    private void set(Object obj, Object val) {
        try {
            if (field != null) {
                field.set(obj, val);
            } else if (setter != null) {
                setter.invoke(obj, val);
            }
        } catch (Exception e) {
            throw new RuntimeException("Exception while setting value : " + (field != null ? field : setter), e);
        }
    }

    private Object newObject() {
        try {
            return type.newInstance();
        } catch (Exception e) {
            throw new RuntimeException("Exception in initializing : " + type, e);
        }
    }

    public Object unmarshal(String value, XmlBinder xb) {
        if (value == null) return null;
        if (type == String.class) return value;
        value = value.trim();
        if (type == Integer.class || type == int.class) return Integer.parseInt(value);
        if (type == Float.class || type == float.class) return Float.parseFloat(value);
        if (type == Long.class || type == long.class) return Long.parseLong(value);
        if (type == Double.class || type == double.class) return Double.parseDouble(value);
        if (type == BigDecimal.class) return new BigDecimal(value);
        if (type == Short.class || type == short.class) return Short.parseShort(value);
        if (type == Byte.class || type == byte.class) return Byte.parseByte(value);
        if (type == Boolean.class || type == boolean.class)
            return ("true".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value)) ?
                    Boolean.TRUE : Boolean.FALSE;
        try {
            if (type == Timestamp.class)
                return DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).parse(value);
        } catch (ParseException e) {
            throw new RuntimeException("Inavlid date format", e);
        }

        return null;
    }

    /**
     * Fetches the value of a field depending on whether it is a getter method or a field
     *
     * @param obj
     * @return
     */
    public Object getFieldValue(Object obj) {
        if (isEmpty) return obj;
        try {
            if (field != null) return field.get(obj);
        } catch (Exception e) {
            throw new RuntimeException("Failed to access field : " + field, e);
        }
        if (getter != null) {
            try {
                getter.invoke(obj);
            } catch (Exception e) {
                throw new RuntimeException("Failed invoking the getter method: " + getter, e);
            }
        }
        return null;
    }

    public void injectElem(XmlPullParser xpp, Object obj, String parentTagName, XmlBinder xb)
            throws IOException, XmlPullParserException {
        if (isEmpty) {
            bean.inject(xpp, obj, parentTagName, xb);
            return;
        }
        Object newObject = newObject();
        if (bean.setParentMethod != null) {
            try {
                bean.setParentMethod.invoke(newObject, obj);
            } catch (Exception e) {
                //todo report the exception
            }
        }
        bean.inject(xpp, newObject, parentTagName, xb);
        checkTypeAndSet(obj, newObject, null, xb);
    }

    /**
     * Handles special handling of different types such as arrays , lists, collections etc
     *
     * @param obj    The object on which the value is to be injected
     * @param theVal The value to be injected
     * @param name
     * @param xb
     */
    void checkTypeAndSet(Object obj, Object theVal, String name, XmlBinder xb) {
        if (theVal == null) return;
        if (isSet) {
            ((Collection) checkAndInitCollectionType(obj, HashSet.class)).add(theVal);
        } else if (isCollection) {
            ((Collection) checkAndInitCollectionType(obj, ArrayList.class)).add(theVal);
        } else if (isProp) {
            ((Properties) checkAndInitCollectionType(obj, Properties.class)).put(name, theVal);
        } else if (isMap) {
            Map map = (Map) checkAndInitCollectionType(obj, HashMap.class);
            Object o = map.get(name);
            if (o == null) {
                map.put(name, theVal);
            } else {
                List list = null;
                if (o instanceof List) {
                    list = (List) o;
                } else {
                    list = new ArrayList();
                    list.add(o);
                    map.put(name, list);
                }
                list.add(theVal);
            }
        } else if (isArray) {
            Object[] vals = (Object[]) getFieldValue(obj);
            int len = vals == null ? 0 : vals.length;
            Object[] newArr = (Object[]) Array.newInstance(type, len + 1);
            newArr[len] = theVal;
            if (vals != null) System.arraycopy(vals, 0, newArr, 0, len);
            set(obj, newArr);
        } else {
            set(obj, theVal);
        }
    }

    /**
     * If the field to be set is of a collection type initialize it
     *
     * @param obj
     * @param collectionType
     * @return
     */
    private Object checkAndInitCollectionType(Object obj, Class collectionType) {
        Object l = getFieldValue(obj);
        if (l == null) {
            try {
                l = collectionType.newInstance();
            } catch (Exception e) { /*can never happen*/ }
            set(obj, l);
        }
        return l;
    }

    public void unbind(TagStack t, Object obj, XmlSerializer ser, XmlUnbinder xub)
            throws IOException {
        Object val = null;
        Object[] arrVal = null;
        val = getFieldValue(obj);
        if (val == null) return;
        if (!isEmpty && (isArray || isCollection)) {
            if (isArray) {
                arrVal = (Object[]) val;
            } else if (isCollection) {
                Collection collection = (Collection) val;
                arrVal = collection.toArray(new Object[0]);
            }
        } else {
            arrVal = new Object[1];
            arrVal[0] = val;
        }
        if (isAttr) {
            t.attr(name, val.toString(), ser);
        } else if (isCdata) {
            t.cdata(val.toString(), ser);
        } else {
            for (int i = 0; i < arrVal.length; i++) {
                if (arrVal[i] == null) continue;
                TagStack tag = new TagStack(name);
                t.push(tag, ser);
                if (bean != null && bean.isSimpleType) {
                    tag.cdata(arrVal[i].toString(), ser);
                } else {
                    bean.unbind(tag, arrVal[i], ser, xub);
                }
                t.pop(ser);
            }
        }
    }
}
